#
#      Copyright 2011 Battams, Derek
#       
#       Licensed under the Apache License, Version 2.0 (the "License");
#       you may not use this file except in compliance with the License.
#       You may obtain a copy of the License at
#
#          http://www.apache.org/licenses/LICENSE-2.0
#
#       Unless required by applicable law or agreed to in writing, software
#       distributed under the License is distributed on an "AS IS" BASIS,
#       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#       See the License for the specific language governing permissions and
#       limitations under the License.
#
#	$Id: sage.py 63 2011-08-02 13:19:43Z derek@battams.ca $

'''A python interface to the sagex SageTV HTTP API.

This module provides a natural python look and feel to the sagex HTTP API and
allows you to interact with your SageTV system from python scripts.

This module has the following external requirements:

	* The sagex remote API plugin must be installed on your SageTV server
	  and the HTTP interface must be enabled.
	* If you're using a python version older than 2.6 then you must include
	  the simplejson package and ensure it's accessible at runtime.  When
	  executed under python 2.6, this module will use the builtin json 
	  package for JSON processing.
	* To use any of the module mediaFileTo*() methods, you must be running
	  in the Boxee environment.  If you're not and attempt to use any of
	  these methods they will simply return None.
'''
import urllib, logging, re, threading
try:
	import simplejson as json # For older versions of python, including that used in Boxee environment
except ImportError:
	import json
import os.path as path
from threading import RLock
from datetime import date
from time import sleep
try:
	import mc
	IS_BOXEE_ENV = True
except ImportError:
	IS_BOXEE_ENV = False

def _unicodeToStr(obj):
	'''Convert all unicode strings in a JSON object to 8-bit ascii encoded utf-8 strings'''
	t = type(obj)
	if(t is unicode):
		return obj.encode(DEFAULT_CHARSET)
	elif(t is list):
		for i in range(0, len(obj)):
			obj[i] = _unicodeToStr(obj[i])
		return obj
	elif(t is dict):
		for k in obj.keys():
			v = obj[k]
			del obj[k]
			obj[k.encode(DEFAULT_CHARSET)] = _unicodeToStr(v)
		return obj
	else:
		return obj # leave numbers and booleans alone

def _isValidResponse(resp):
	return isinstance(resp, dict) and not _isValidError(resp)

def _isValidError(resp):
	return isinstance(resp, dict) and resp.has_key(PROP_ERROR) and resp.has_key(PROP_EXCEPTION)

def objToListItem(obj):
	'''Convert an arbitrary python object to a Boxee ListItem object.

		This function simply creates a ListItem with the Label set to the str() value of obj.

		obj: Any object to be converted to a ListItem
		return: A ListItem with it's label set to the str() of the argument
	'''
	if(not IS_BOXEE_ENV):
		return None
	item = mc.ListItem(mc.ListItem.MEDIA_VIDEO_EPISODE)
	try:
		item.SetLabel(str(obj))
	except UnicodeEncodeError:
		item.SetLabel(obj.encode(DEFAULT_CHARSET))
	except Exception, e:
		LOG.LogError('Unknown error: %s', e)
	return item

def objListToListItems(list):
	'''Convert a list of arbitrary python objects to a Boxee ListItems object.

		This function simply creates a ListItems obj containing the converted elements of the given list argument.

		list: A list of objects.
		return: A ListItems obj where each element of list is converted to a ListItem and pushed onto the ListItems obj
	'''
	if(not IS_BOXEE_ENV):
		return None
	items = mc.ListItems()
	for i in list:
		item = objToListItem(i)
		if(item != None):
			items.append(item)
	return items

def mediaFileToListItem(mf):
	'''Convert a Sage MediaFile JSON object to a Boxee ListItem object.

		    mf: A dictionary representing a SageTV MediaFile JSON object
		return: A Boxee ListItem object representing the given MediaFile object
	'''
	if(not IS_BOXEE_ENV):
		return None
	return _tvMediaFileToListItem(mf)

def _tvMediaFileToListItem(mf):
	try:
		from config import mediaPathMapper

		item = mc.ListItem(mc.ListItem.MEDIA_VIDEO_EPISODE)
		item.SetProperty('MediaFileID', str(mf['MediaFileID']))
		item.SetLabel(mf['MediaTitle'])
		item.SetTVShowTitle(mf['MediaTitle'])
		item.SetPath(mediaPathMapper.convertPath(mf['SegmentFiles'][0]))
		item.SetContentType('video/' + path.splitext(item.GetPath())[1][1:])
		item.SetDuration(mf['FileDuration'] // 1000)
		item.SetSize(mf['Size'])
		item.SetAddToHistory(True)
		if (mf['IsLibraryFile']):
			item.SetProperty('IsLibraryFile', str(mf['IsLibraryFile']))
		item.SetProperty('MediaFileEncoding', mf['MediaFileEncoding'])
		item.SetProperty('MediaFileFormatDescription', mf['MediaFileFormatDescription'])
		item.SetProperty('FormattedDuration', _getHoursMinutes(mf['FileDuration'] // 1000))
		if(mf.has_key('Airing') and mf['Airing'].has_key('Show')):
			show = mf['Airing']['Show']
			item.SetProperty('ShowExternalID', show['ShowExternalID'])
			if(show.has_key('ShowYear') and show['ShowYear'] != ''):
				item.SetYear(int(show['ShowYear']))
			if(show.has_key('RolesInShow')):
				roles = show['RolesInShow']
				try:
					# ListItem only allows for one Writer credit, sorry to the rest of you!
					i = roles.index('Writer')
					item.SetWriter(show['PeopleListInShow'][i])
				except ValueError:
					pass

				try:
					i = roles.index('Director')
					item.SetDirector(show['PeopleListInShow'][i])
				except ValueError:
					pass

				skippedRoles = ['Writer', 'Director']
				for role in roles:
					if(role in skippedRoles):
						continue
					skippedRoles.append(role)
					LOG.LogDebug('Adding role "%s"...' % role)
					i = -1
					while 1:
						try:
							i = roles.index(role, i + 1)
							person = show['PeopleAndCharacterListInShow'][i]
							item.AddCastAndRole(person, role)
							#LOG.LogDebug('Added "%s" to role "%s"' % (person, role))
						except ValueError:
							break

			if(show.has_key('ShowEpisode') and show['ShowEpisode'] != ''):
				item.SetTitle(show['ShowEpisode'])
			if(show.has_key('OriginalAiringDate') and show['OriginalAiringDate'] > 0):
				origDate = date.fromtimestamp(float(show['OriginalAiringDate'] // 1000))
				item.SetDate(origDate.year, origDate.month, origDate.day)
			if(show.has_key('ShowEpisode') and show['ShowEpisode'] != ''):
				item.SetTitle(show['ShowEpisode'])
			if(show.has_key('ShowCategoriesString') and show['ShowCategoriesString'] != ''):
				item.SetGenre(show['ShowCategoriesString'])
			if(show.has_key('ShowSeasonNumber') and show['ShowSeasonNumber'] > 0):
				item.SetSeason(show['ShowSeasonNumber'])
			if(show.has_key('ShowEpisodeNumber') and show['ShowEpisodeNumber'] > 0):
				item.SetEpisode(show['ShowEpisodeNumber'])
			if(show.has_key('ShowDescription') and show['ShowDescription'] != ''):
				item.SetDescription(show['ShowDescription'])
			if(show.has_key('IsShowFirstRun') and show['IsShowFirstRun']):
				item.SetProperty('IsShowFirstRun', str(show['IsShowFirstRun']))
				print 'SAGEBOXEE:: Setting IsShowFirstRun to ' + str(show['IsShowFirstRun'])
			if(show.has_key('ShowRated') and show['ShowRated'] != ''):
				item.SetContentRating(show['ShowRated'])

		if(mf.has_key('Airing')):
			airing = mf['Airing']
			item.SetProperty('AiringID', str(airing['AiringID']))
			if(airing.has_key('IsWatched') and airing['IsWatched']):
				item.SetProperty('IsWatched', str(airing['IsWatched']))
			if (airing.has_key('IsAiringHDTV') and airing['IsAiringHDTV']):
				item.SetProperty('IsHDTV', 'True')
			if(airing.has_key('ExtraAiringDetails')):
				item.SetProperty('ExtraAiringDetails', airing['ExtraAiringDetails'])
				if (airing['ExtraAiringDetails'].find('HDTV') != -1):
					item.SetProperty('IsHDTV', 'True')
			if(airing.has_key('ParentalRating') and airing['ParentalRating'] != ''):
				item.SetContentRating(airing['ParentalRating'])
			if(airing.has_key('AiringChannelName')):
				item.SetStudio(airing['AiringChannelName'])
				item.SetProperty('AiringChannelName', airing['AiringChannelName'])
			if(airing.has_key('AiringChannelNumber')):
				item.SetProperty('AiringChannelNumber', airing['AiringChannelNumber'])
			if(airing.has_key('IsFavorite') and airing['IsFavorite']):
				item.SetProperty('IsFavorite', str(airing['IsFavorite']))
			if(airing.has_key('IsManualRecord') and airing['IsManualRecord']):
				item.SetProperty('IsManualRecord', str(airing['IsManualRecord']))
			if(airing.has_key('IsDontLike') and airing['IsDontLike']):
				item.SetProperty('IsDontLike', str(airing['IsDontLike']))
			if(airing.has_key('IsWatchedCompletely') and airing['IsWatchedCompletely']):
				item.SetProperty('IsWatchedCompletely', str(airing['IsWatchedCompletely']))
			if(airing.has_key('IsNotManualOrFavorite')):
				item.SetProperty('IsNotManualOrFavorite', str(airing['IsNotManualOrFavorite']))
			if(airing.has_key('AiringStartTime')):
				origDate = date.fromtimestamp(float(airing['AiringStartTime'] // 1000))
				item.SetProperty('AiringStartTime', str(origDate.month) + '/' + str(origDate.day) + '/' + str(origDate.year))
		return item
	except IndexError:
		LOG.LogDebug('sage.py: Received MediaFile with no file segments [%d]' % mf['MediaFileID'])
		return None

def _movieMediaFileToListItem(mf):
	pass

def mediaFileListToListItemList(list):
	'''Convert a list of Sage MediaFile JSON objects to a list of Boxee ListItem objects.

		  list: A list of dictionary objects; each dictionary represents a Sage MediaFile JSON object
		return: A list of Boxee ListItem objects
	'''
	items = []
	for i in list:
		item = mediaFileToListItem(i)
		if(item != None):
			items.append(item)
	return items

def mediaFileListToListItems(list):
	'''Convert a list of Sage MediaFile JSON objects to a Boxee ListItems object.

		  list: A list of dictionary objects; each dictionary represents a Sage MediaFile
		return: A Boxee ListItems object; the contents is each dictionary converted to a Boxee ListItem
	'''
	if(not IS_BOXEE_ENV):
		return None
	items = mc.ListItems()
	for i in list:
		item = mediaFileToListItem(i)
		if(item != None):
			items.append(item)
	return items

def _getHoursMinutes(seconds):
	'''Convert seconds to hours and minutes.'''
	m, s = divmod(seconds, 60)
	if (s > 30):
		m += 1
	h, m = divmod(m, 60)
	mstring = (m > 0) and ('%d ' % (m)) + _getPlural(m, 'minute') or ''
	hstring = (h > 0) and ('%d ' % (h)) + _getPlural(h, 'hour') or ''
	return ('%s %s' % (hstring, mstring)).strip()

def _getPlural(count, label):
	return label + ((count > 1) and 's' or '')

class SageApi:
	'''A python wrapper for the SageTV API; allows API calls via the sagex remote HTTP API interface.

		Typical usage:

		from sage import SageApi

		api = SageApi('192.168.0.10:8080', 'myid', 'mypwd')
		print api.GetOS() # Prints 'Windows XP' on my system
		print api.GetMediaFiles('T') # Prints all MediaFile objects that are TV recordings

		See the /sagex/api page on your SageTV Jetty server for details on all available API calls.
	'''
	_host = None
	_id = None
	_pwd = None
	_ssl = False
	_auth = None

	def __init__(self, host = 'localhost', id = None, pwd = None, ssl = False):
		'''Constructor.

			host: The SageTV hostname to connect to; if the port is not 80 or 443 (if using SSL) then you must also supply the port number by adding ':port' to the end of the host name (i.e. 192.168.1.10:8080)
			  id: The Jetty username to conenct as; if your sagex API is not protected via id/pwd then you don't need to provide this, but you really should protect it!
			 pwd: The Jetty password to connect with
			 ssl: If true, connect to the API via SSL (encrypted connection) otherwise just use a standard HTTP connection; default is false (i.e. don't use SSL); your Jetty plugin must be configured for SSL if you wish to use it here
		'''
		self._host = host
		self._id = id
		self._pwd = pwd
		self._ssl = ssl
		LOG.LogDebug('sagex API Creds: %s' % ((host, id, '*****', ssl),))

	def __getattr__(self, name):
		'''Converts an attribute request to a single sagex HTTP API call.

			  name: The name of the Sage API call to be made
			return: A callable python object capable of making the Sage API call as identified by the given attribute name
		'''
		if(re.match('[a-z]', name[0])):
			return _SageApiHelpers(self._host, self._id, self._pwd, self._ssl).__getattr__(name)
		else:
			LOG.LogDebug('Dispatching sagex API call: %s' % name)
			return _SagexHttpApi(name, self._host, self._id, self._pwd, self._ssl)

class _SageApiHelpers:
	_host = None
	_id = None
	_pwd = None
	_ssl = False

	def __init__(self, host, id, pwd, ssl):
		self._host = host
		self._id = id
		self._pwd = pwd
		self._ssl = ssl

	def __getattr__(self, name):
		api = SageApi(self._host, self._id, self._pwd, self._ssl)
		if(name == 'getUniqueTvTitles'):
			return _dummyCallable(api.EvaluateExpression('Sort(java_util_Map_keySet(GroupByMethod(GetMediaFiles("T"),"GetMediaTitle")),false,"Natural")'))
		elif(name == 'isRecordingSomething'):
			return _dummyCallable(api.EvaluateExpression('Size(GetCurrentlyRecordingMediaFiles()) > 0'))
		elif(name == 'isApiServerAlive'):
			return _dummyCallable(api.EvaluateExpression('Size(GetOS()) > 0'))
		elif(name == 'getMediaFilesForTitle'):
			return _apiExprEvaluator(api, 'FilterByMethod(GetMediaFiles("T"), "GetMediaTitle", "%s", true)')
		else:
			raise SageApiIoException('Invalid Sage API call made! [%s]' % name)

class _dummyCallable:
	''' Simulate callable objects for internal API helper methods.'''
	_rc = None

	def __init__(self, rc):
		self._rc = rc

	def __call__(self, *args):
		return self._rc

class _apiExprEvaluator:

	_api = None
	_expr = None

	def __init__(self, api, expr):
		self._api = api
		self._expr = expr

	def __call__(self, *args):
		return self._api.EvaluateExpression(self._expr % args)

class _SagexHttpApi:
	'''Encapsulates a single sagex remote HTTP API call.'''
	_name = None
	_host = None
	_id = None
	_pwd = None
	_ssl = None

	def __init__(self, name, host, id, pwd, ssl):
		self._name = name
		self._host = host
		self._id = id
		self._pwd = pwd
		self._ssl = ssl

	def __call__(self, *args):
		url = 'http'
		if(self._ssl == True):
			url += 's'
		url += '://'
		if(self._id != None and self._pwd != None):
			url += self._id + ':' + self._pwd + '@'
		url += self._host + '/sagex/api?size=50000&encoder=json&c=' + self._name
		for i in range(0, len(args)):
			try:
				payload = str(args[i])
			except UnicodeEncodeError:
				payload = args[i].encode(DEFAULT_CHARSET)
			url += '&' + str(i + 1) + '=' + urllib.quote_plus(payload)
		LOG.LogDebug('sagex request URL: %s' % url)
		try:
			input = _SageApiFancyUrlOpener().open(url)
		except IOError, e:
			raise SageApiIoException('Network error', e)
		# I highly doubt hard coding this response encoding is going to hold up for too long :(
		payload = input.read()
		resp = _unicodeToStr(json.JSONDecoder('ISO-8859-1').decode(payload))
		if(_isValidResponse(resp)):
			LOG.LogInfo('sagex API call "%s" [args: %s] succeeded!' % (self._name, args))
			LOG.LogDebug('sagex API call: valid response received (1KB max logged)\n\n%s' % payload[0:1024])
			if(len(resp.keys()) != 1):
				raise SageApiIoException('Unrecognized response!')
			return resp[resp.keys()[0]]
		elif(_isValidError(resp)):
			LOG.LogInfo('sagex API call "%s" [args: %s] FAILED!' % (self._name, args))
			raise SageApiException(resp)
		else:
			LOG.LogInfo('sagex API call "%s" [args: %s] generated unrecognized response!' % (self._name, args))
			LOG.LogDebug('sagex API call: INVALID response received!\n\n%s' % payload)
			raise SageApiIoException(resp)

class SageApiException(Exception):
	'''Thrown when a sagex API call returns an error response.'''

	_resp = None
	_msg = None
	_err = None
	_exception = None

	def __init__(self, errResp, msg = None):
		self._resp = errResp
		self._msg = msg
		self._err = errResp[PROP_ERROR]
		self._exception = errResp[PROP_EXCEPTION]
		LOG.LogError('sagex API error response:\nMSG: %s\nERROR: %s' % (self._msg, self._err))
		LOG.LogDebug('sagex API exception:\n%s' % self._exception)

	def __str__(self):
		return self._err

class SageApiIoException(SageApiException):
	'''Thrown when an IO error occurs during an API call; usually a network error.'''

	_data = None
	_cause = None
	_msg = None

	def __init__(self, data, cause = None):
		self._data = data
		self._cause = cause
		msg = self._data
		if(self._cause != None):
			msg += ' [cause: ' + str(self._cause) + ']'
		self._msg = msg
		LOG.LogError('SageApiIOException: %s' % msg)

	def __str__(self):
		return self._msg

class _SageApiFancyUrlOpener(urllib.FancyURLopener):
	'''Extends FancyURLopener by simply throwing an exception in response to a 401 from the remote server.'''
	def prompt_user_passwd(self, host, realm):
		raise SageApiIoException('Invalid Sagex API id or password!')

class _SageLogger:

	_log = None

	def __init__(self, target = None):
		if(IS_BOXEE_ENV):
			self._log = mc
		else:
			logging.basicConfig(filename = target, level = logging.DEBUG)
			self._log = _SageDebugLogger()

	def LogError(self, msg):
		try:
			self._log.LogError(msg)
		except TypeError:
			self._log.LogError(msg.encode(DEFAULT_CHARSET))

	def LogInfo(self, msg):
		try:
			self._log.LogInfo(msg)
		except TypeError:
			self._log.LogInfo(msg.encode(DEFAULT_CHARSET))

	def LogDebug(self, msg):
		try:
			self._log.LogDebug(msg)
		except TypeError:
			self._log.LogDebug(msg.encode(DEFAULT_CHARSET))

class _SageDebugLogger:

	def LogError(self, msg):
		logging.error(msg)

	def LogInfo(self, msg):
		logging.info(msg)

	def LogDebug(self, msg):
		logging.debug(msg)
		
class IconStatusThread(threading.Thread):
	_status = dict()
	_keepAlive = True
	_api = None
	_lock = None
	_activeWindow = 14000

	def __init__(self, api):
		threading.Thread.__init__(self)
		self._api = api
		self._lock = RLock()

	def run(self):
		LOG.LogInfo('SageBoxee: sage.py: Icon status thread started.')
		iconPath = mc.GetApp().GetAppMediaDir() + "/media/"
		while 1:
			try:
				self._lock.acquire()
				try:
				
					# Set visibility of Conflict icon.
					self._status['IsUnresolvedConflicts'] = self._api.AreThereUnresolvedConflicts()
					mc.GetWindow(self._activeWindow).GetControl(902).SetVisible(self._status['IsUnresolvedConflicts'])
					
					# Set visibility of Recording icon.
					self._status['IsRecording'] = self._api.isRecordingSomething()
					mc.GetWindow(self._activeWindow).GetControl(901).SetVisible(self._status['IsRecording'])
					
					# Set visibility of Library Import Scan icon.
					self._status['IsScanning'] = self._api.IsDoingLibraryImportScan()
					mc.GetWindow(self._activeWindow).GetControl(900).SetVisible(self._status['IsScanning'])
					
					# Set alert level.
					self._status['AlertLevel'] = self._api.GetSystemAlertLevel()
					statusControl = mc.GetWindow(self._activeWindow).GetControl(899)
					statusIcon = mc.GetWindow(self._activeWindow).GetImage(899)
					#statusLevel = api.GetSystemAlertLevel()
					statusLevel = self._status['AlertLevel']
					if (statusLevel == 0):
						statusControl.SetVisible(False)
					elif (statusLevel == 1):
						statusIcon.SetTexture(iconPath + "MarkerSysAlert1.png")
						statusControl.SetVisible(True)
					elif (statusLevel == 2):
						statusIcon.SetTexture(iconPath + "MarkerSysAlert2.png")
						statusControl.SetVisible(True)
					elif (statusLevel == 3):
						statusIcon.SetTexture(iconPath + "MarkerSysAlert3.png")
						statusControl.SetVisible(True)
					

				except Exception, e:
					LOG.LogError('Error updating icon status! [%s]' % str(e))
				if(not self._keepAlive):
					break
			finally:
				self._lock.release()
			sleep(10)
		LOG.LogInfo('SageBoxee: sage.py: Icon status thread has terminated!')

	def getStatus(self, id = None):
		self._lock.acquire()
		if(id == None):
			rc = self._status.copy()
		else:
			rc = self._status[id]
		self._lock.release()
		return rc

	def setKeepAlive(self, val):
		self._lock.acquire()
		self._keepAlive = val
		self._lock.release()
		
	def setActiveWindow(self, val):
		self._lock.acquire()
		LOG.LogInfo('SageBoxee: sage.py: New activeWindow is ' + str(val))
		self._activeWindow = val
		self._lock.release()
		
def stopIconStatusThread(t):
	t.setKeepAlive(False)

LOG             = _SageLogger()

PROP_ERROR      = 'error'
PROP_EXCEPTION  = 'exception'

DEFAULT_CHARSET = 'utf-8'